package com.swak.fx.support.ui;

import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;

import com.swak.fx.support.animation.AnimateFXInterpolator;
import com.swak.fx.support.ui.UI.ScrollDirection;

import javafx.animation.Animation;
import javafx.animation.Animation.Status;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Skin;
import javafx.scene.control.skin.ScrollPaneSkin;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;

public class UIScrollPaneInner extends ScrollPane implements UILifecycle {

	// ================================================================================
	// Properties
	// ================================================================================
	private final String STYLE_CLASS = "ui-scroll-pane";
	private final String STYLESHEET = UI.load("css/UIScrollPane.css");

	public UIScrollPaneInner() {
		init();
	}

	// ================================================================================
	// Methods
	// ================================================================================
	@Override
	public void initializeParts() {
		getStyleClass().setAll(STYLE_CLASS);
		// 这两端代码很关键
		// 不然会导致未设置大小的父节点撑大
		this.setPrefHeight(0);
		this.setPrefWidth(0);
		this.setMinHeight(0);
		this.setMinWidth(0);

		timeline.getKeyFrames().add(new KeyFrame(Duration.millis(3), event -> {
			for (int i = 0; i < derivatives.length; i++) {
				derivatives[i] *= frictions[i];
			}
			for (int i = 1; i < derivatives.length; i++) {
				derivatives[i] += derivatives[i - 1];
			}

			double dy = derivatives[derivatives.length - 1];
			Function<Bounds, Double> sizeFunction = (scrollDirection.get() == ScrollDirection.UP
					|| scrollDirection.get() == ScrollDirection.DOWN) ? Bounds::getHeight : Bounds::getWidth;
			double size = sizeFunction.apply(this.getContent().getLayoutBounds());
			double value;
			switch (scrollDirection.get()) {
			case LEFT:
			case RIGHT:
				value = Math.min(Math.max(this.hvalueProperty().get() + dy / size, 0), 1);
				this.hvalueProperty().set(value);
				break;
			case UP:
			case DOWN:
				value = Math.min(Math.max(this.vvalueProperty().get() + dy / size, 0), 1);
				this.vvalueProperty().set(value);
				break;
			}

			if (Math.abs(dy) < 0.001) {
				timeline.stop();
			}
		}));
		timeline.setCycleCount(Animation.INDEFINITE);
	}

	@Override
	public void setupEventHandlers() {
		Node content = this.getContent();
		if (content != null && content.getParent() != null) {
			content.getParent().removeEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
			content.getParent().removeEventHandler(ScrollEvent.ANY, scrollHandler);
			content.getParent().addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
			content.getParent().addEventHandler(ScrollEvent.ANY, scrollHandler);
		}

		if (content != null) {
			content.parentProperty().addListener((ob, oldValue, newValue) -> {
				if (oldValue != null) {
					oldValue.removeEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
					oldValue.removeEventHandler(ScrollEvent.ANY, scrollHandler);
				}
				if (newValue != null) {
					newValue.addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
					newValue.addEventHandler(ScrollEvent.ANY, scrollHandler);
				}
			});
		}
	}

	@Override
	public void setupValueChangedListeners() {
		this.contentProperty().addListener((ob, o, n) -> this.setupEventHandlers());
	}

	// ================================================================================
	// Override Methods
	// ================================================================================
	@Override
	protected Skin<?> createDefaultSkin() {
		return new UIScrollPaneInnerSkin(this);
	}

	@Override
	public String getUserAgentStylesheet() {
		return STYLESHEET;
	}

	// ================================================================================
	// Func
	// ================================================================================
	public void scrollToTop() {
		Timeline berthEventIn = new Timeline(
				new KeyFrame(Duration.millis(0),
						new KeyValue(this.vvalueProperty(), this.vvalueProperty().get(), AnimateFXInterpolator.EASE)),
				new KeyFrame(Duration.millis(500), new KeyValue(this.vvalueProperty(), 0, AnimateFXInterpolator.EASE)));
		berthEventIn.play();
	}

	public void scrollToBottom() {
		this.scrollToBottom(200);
	}

	public void scrollToBottom(long timeMS) {
		if (timeMS > 0) {
			Timeline berthEventIn = new Timeline(
					new KeyFrame(Duration.millis(0),
							new KeyValue(this.vvalueProperty(), this.vvalueProperty().get(),
									AnimateFXInterpolator.EASE)),
					new KeyFrame(Duration.millis(timeMS),
							new KeyValue(this.vvalueProperty(), 1, AnimateFXInterpolator.EASE)));
			berthEventIn.play();
		} else {
			this.vvalueProperty().set(1);
		}
	}

	public void scrollToNode(Node node) {
		Bounds bounds = node.localToParent(node.getBoundsInLocal());
		double height = this.getHeight();
		this.vvalueProperty().set(bounds.getMinY() / height);
	}

	public void scrollToBottom(double bottomHeight) {
		if (this.scrollHeight() > 0) {
			double vvalue = (this.scrollHeight() - bottomHeight) / this.scrollHeight();
			this.vvalueProperty().set(vvalue);
		} else {
			this.vvalueProperty().set(1);
		}
	}

	public double scrollHeight() {
		Node content = this.getContent();
		double scrollHeight = (content != null ? content.prefHeight(-1) : 0) - this.getHeight();
		return Math.max(0, scrollHeight);
	}

	public double scrollTop() {
		double value = this.vvalueProperty().get();
		double scrollHeight = this.scrollHeight();
		return scrollHeight * value;
	}

	public double scrollBottom() {
		return this.scrollHeight() - scrollTop();
	}

	public boolean isScrollBottom(double adjustHeight) {
		return this.scrollBottom() <= adjustHeight;
	}

	// ================================================================================
	// Smooth
	// ================================================================================
	public DoubleProperty speed() {
		return speed;
	}

	public DoubleProperty trackPadAdjustment() {
		return trackPadAdjustment;
	}

	private final DoubleProperty speed = new SimpleDoubleProperty(0.5);
	private final DoubleProperty trackPadAdjustment = new SimpleDoubleProperty(7);

	final double[] frictions = { 0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008,
			0.0006, 0.0005, 0.00003, 0.00001 };
	final double[] derivatives = new double[frictions.length];
	final AtomicReference<Double> atomicSpeed = new AtomicReference<>(speed.get());

	Timeline timeline = new Timeline();
	AtomicReference<ScrollDirection> scrollDirection = new AtomicReference<>();
	final EventHandler<MouseEvent> mouseHandler = event -> timeline.stop();
	final EventHandler<ScrollEvent> scrollHandler = event -> {
		if (event.getEventType() == ScrollEvent.SCROLL) {
			scrollDirection.set(determineScrollDirection(event));
			if (isTrackPad(event, scrollDirection.get())) {
				atomicSpeed.set(speed.get() / trackPadAdjustment.get());
			} else {
				atomicSpeed.set(speed.get());
			}
			derivatives[0] += scrollDirection.get().direction * atomicSpeed.get();
			if (timeline.getStatus() == Status.STOPPED) {
				timeline.play();
			}
		}
	};

	private static ScrollDirection determineScrollDirection(ScrollEvent event) {
		double deltaX = event.getDeltaX();
		double deltaY = event.getDeltaY();

		if (deltaY == 0.0) {
			return deltaX < 0 ? ScrollDirection.LEFT : ScrollDirection.RIGHT;
		} else {
			return deltaY < 0 ? ScrollDirection.DOWN : ScrollDirection.UP;
		}
	}

	private static boolean isTrackPad(ScrollEvent event, ScrollDirection scrollDirection) {
		switch (scrollDirection) {
		case UP:
		case DOWN:
			return Math.abs(event.getDeltaY()) < 10;
		case LEFT:
		case RIGHT:
			return Math.abs(event.getDeltaX()) < 10;
		default:
			return false;
		}
	}

	public static class UIScrollPaneInnerSkin extends ScrollPaneSkin {

		public UIScrollPaneInnerSkin(UIScrollPaneInner scrollPane) {
			super(scrollPane);
			StackPane viewPort = (StackPane) scrollPane.lookup(".viewport");
			viewPort.setCache(false);
		}
	}
}
